﻿using System.Collections.Generic;
using System.IO;
using System.Linq;
using AForge.Neuro;
using AForge.Neuro.Learning;
using System;
using System.Text;

namespace CarEvaluationANN
{
    public class NeuralNetworkController
    {
        #region Props&Fields
        private const string FileDirectory = @"./Statistics/";

        private ActivationNetwork ActNetwork { get; set; }

        private int _cycles = 1000;
        private List<double> _errorList = new List<double>();
        private byte _hiddenLayerCount = 1;
        private double _learningErrorLimit = 0.0001;

        private double _learningRate = 0.3;
        private double _momentum = 0.6;

        private int _neuronCount = 14;
        private double _sigmoidAlphaValue = 2.0;

        public int Cycles
        {
            get { return _cycles; }
            set { _cycles = value; }
        }

        public double LearningRate
        {
            get { return _learningRate; }
            set { _learningRate = value; }
        }

        public int NeuronCount
        {
            get { return _neuronCount; }
            set { _neuronCount = value; }
        }

        public List<double> ErrorList
        {
            get { return _errorList; }
            private set { _errorList = value; }
        }

        public List<DataSetUnit> TrainingSet { get; set; }

        public byte HiddenLayerCount
        {
            get { return _hiddenLayerCount; }
            set { _hiddenLayerCount = value; }
        }

        public double SigmoidAlphaValue
        {
            get { return _sigmoidAlphaValue; }
            set { _sigmoidAlphaValue = value; }
        }

        public double Momentum
        {
            get { return _momentum; }
            set { _momentum = value; }
        }

        public double LearningErrorLimit
        {
            get { return _learningErrorLimit; }
            set { _learningErrorLimit = value; }
        }

        public bool SaveStatisticsToFile { get; set; }

        private int InputLength
        {
            get { return TrainingSet.First().Input.Length; }
        }

        private int OutputLength
        {
            get { return TrainingSet.First().Output.Length; }
        }

        public event EventHandler<EpochEndedEventArgs> EpochEnded;

        private Guid _currentGuid = Guid.NewGuid();
        public Guid CurrentGuid
        {
            get { return _currentGuid; }
            set { _currentGuid = value; }
        }
        #endregion

        #region Constructors
        public NeuralNetworkController(List<DataSetUnit> trainingSet)
        {
            TrainingSet = trainingSet;
        }
        #endregion

        #region Methods
        public void Train()
        {
            ActNetwork = new ActivationNetwork(new SigmoidFunction(SigmoidAlphaValue),
                                                InputLength, NeuronCount, OutputLength);

            var teacher = new BackPropagationLearning(ActNetwork) { LearningRate = LearningRate, Momentum = Momentum };

            try
            {
                var input = TrainingSet.Select(t => t.Input).ToArray();
                var output = TrainingSet.Select(t => t.Output).ToArray();

                for (var i = 0; i < Cycles; i++)
                {
                    var error = teacher.RunEpoch(input, output);
                    ErrorList.Add(error);

                    if (EpochEnded != null)
                    {
                        EpochEnded(this, new EpochEndedEventArgs { Error = error });
                    }

                    if (error <= LearningErrorLimit)
                        break;
                }

                if (SaveStatisticsToFile)
                {
                    SaveToFiles();
                }
            }
            catch (IOException)
            {
            }
        }

        private void SaveToFiles()
        {
            if (!Directory.Exists(FileDirectory))
                Directory.CreateDirectory(FileDirectory);
            var strBuilder = new StringBuilder();
            ErrorList.ForEach(t => strBuilder.AppendLine(t.ToString()));
            CurrentGuid = Guid.NewGuid();
            File.WriteAllText(String.Format(@"{0}{1}.csv", FileDirectory, CurrentGuid), strBuilder.ToString());

            var summaryContent =
                String.Format(
                    @"Instance Count:{8}, Iteration:{0}, Learning Rate:{1}, Neuron Count:{2}, Sigmoid Alpha:{3}, Momentum:{4}, MinError:{5}, Guid:{6} {7}",
                    Cycles, LearningRate, NeuronCount, SigmoidAlphaValue, Momentum, ErrorList.Min(), CurrentGuid,
                    Environment.NewLine, TrainingSet.Count);
            File.AppendAllText(String.Concat(FileDirectory, "log.txt"), summaryContent);

            ActNetwork.Save(String.Format(@"{0}{1}.bin", FileDirectory, CurrentGuid));
        }

        private IEnumerable<double> TestSingle(double[] input)
        {
            return ActNetwork.Compute(input).ToList();
        }
        public string Test(IEnumerable<DataSetUnit> testSet)
        {
            var strBuilder = new StringBuilder();
            strBuilder.AppendLine("Actual;Obtained1;Obtained2;Equality");
            var successfulUnitCount = 0;
            foreach (var dataSetUnit in testSet)
            {
                var obtainedOutputs = TestSingle(dataSetUnit.Input);
                var outputStr = String.Join(",", dataSetUnit.Output);
                var obtainedOutputStr = String.Join(",", obtainedOutputs);
                var obtainedOutputStr2 = String.Join(",", TsFunc(obtainedOutputs));
                var isSuccessful = outputStr.Equals(obtainedOutputStr2);
                
                if (isSuccessful)
                    successfulUnitCount++;
                
                strBuilder.AppendLine(String.Format("{0};{1};{2};{3}", outputStr, obtainedOutputStr, obtainedOutputStr2, isSuccessful));
            }

            if (!Directory.Exists(FileDirectory))
                Directory.CreateDirectory(FileDirectory);
            var filePath = String.Format(@"{0}{1}_test.csv", FileDirectory, CurrentGuid);
            File.WriteAllText(filePath, strBuilder.ToString());
            var successRate = (double)successfulUnitCount / testSet.Count();
            return String.Format("Test Success Rate:{0:P}", successRate);
        }

        public void Load(string path)
        {
            if (!String.IsNullOrEmpty(path))
            {
                ActNetwork = (ActivationNetwork)Network.Load(path);
                CurrentGuid = Guid.Parse(Path.GetFileNameWithoutExtension(path));
            }
        }

        private static IEnumerable<double> TsFunc(IEnumerable<double> output)
        {
            return output.Select(d => d >= 0.9 ? 1 : 0).Select(d => (double)d).ToArray();
        }
        #endregion
    }
}